/**********************************************************************

Audacity: A Digital Audio Editor

SelectHandle.cpp

Paul Licameli split from TrackPanel.cpp

**********************************************************************/

#include "SelectHandle.h"

#include "Scrubbing.h"
#include "ChannelView.h"

#include "AColor.h"
#include "CommandManager.h"
#include "SpectrumAnalyst.h"
#include "LabelTrack.h"
#include "../playabletrack/wavetrack/ui/PitchAndSpeedDialog.h"
#include "NumberScale.h"
#include "Project.h"
#include "ProjectAudioIO.h"
#include "ProjectHistory.h"
#include "../../ProjectSettings.h"
#include "../../ProjectWindow.h"
#include "../../RefreshCode.h"
#include "../../SelectUtilities.h"
#include "SelectionState.h"
#include "SyncLock.h"
#include "../../TrackArt.h"
#include "../../TrackArtist.h"
#include "TrackFocus.h"
#include "../../TrackPanel.h"
#include "../../TrackPanelDrawingContext.h"
#include "../../TrackPanelMouseEvent.h"
#include "ViewInfo.h"
#include "WaveChannelUtilities.h"
#include "WaveClip.h"
#include "WaveTrack.h"
#include "SpectrogramSettings.h"
#include "../../../images/Cursors.h"

// Only for definition of SonifyBeginModifyState:
//#include "NoteTrack.h"

enum {
    //This constant determines the size of the horizontal region (in pixels) around
    //the right and left selection bounds that can be used for horizontal selection adjusting
    //(or, vertical distance around top and bottom bounds in spectrograms,
    // for vertical selection adjusting)
    SELECTION_RESIZE_REGION = 3,

    // Seems 4 is too small to work at the top.  Why?
    FREQ_SNAP_DISTANCE = 10,
};

// #define SPECTRAL_EDITING_ESC_KEY

bool SelectHandle::IsDragging() const
{
    return mSelectionStateChanger.get() != NULL;
}

namespace {
/// Converts a frequency to screen y position.
wxInt64 FrequencyToPosition(const WaveChannel& wc,
                            double frequency,
                            wxInt64 trackTopEdge,
                            int trackHeight)
{
    const auto& settings = SpectrogramSettings::Get(wc);
    float minFreq, maxFreq;
    SpectrogramBounds::Get(wc).GetBounds(wc, minFreq, maxFreq);
    const NumberScale numberScale(settings.GetScale(minFreq, maxFreq));
    const float p = numberScale.ValueToPosition(frequency);
    return trackTopEdge + wxInt64((1.0 - p) * trackHeight);
}

/// Converts a position (mouse Y coordinate) to
/// frequency, in Hz.
double PositionToFrequency(const WaveChannel& wc,
                           bool maySnap,
                           wxInt64 mouseYCoordinate,
                           wxInt64 trackTopEdge,
                           int trackHeight)
{
    const double rate = wc.GetRate();

    // Handle snapping
    if (maySnap
        && mouseYCoordinate - trackTopEdge < FREQ_SNAP_DISTANCE) {
        return rate;
    }
    if (maySnap
        && trackTopEdge + trackHeight - mouseYCoordinate < FREQ_SNAP_DISTANCE) {
        return -1;
    }

    const auto& settings = SpectrogramSettings::Get(wc);
    float minFreq, maxFreq;
    SpectrogramBounds::Get(wc).GetBounds(wc, minFreq, maxFreq);
    const NumberScale numberScale(settings.GetScale(minFreq, maxFreq));
    const double p = double(mouseYCoordinate - trackTopEdge) / trackHeight;
    return numberScale.PositionToValue(1.0 - p);
}

template<typename T>
inline void SetIfNotNull(T* pValue, const T Value)
{
    if (pValue == NULL) {
        return;
    }
    *pValue = Value;
}

// This returns true if we're a spectral editing track.
inline bool isSpectralSelectionView(const ChannelView& channelView)
{
    const WaveChannel* pChannel{};
    return
        channelView.IsSpectral()
        && (pChannel = channelView.FindChannel<const WaveChannel>().get())
        && SpectrogramSettings::Get(*pChannel).SpectralSelectionEnabled();
}

enum SelectionBoundary {
    SBNone,
    SBLeft, SBRight,
    SBBottom, SBTop, SBCenter, SBWidth,
};

SelectionBoundary ChooseTimeBoundary
(
    const double t0, const double t1,
    const ViewInfo& viewInfo,
    double selend, bool onlyWithinSnapDistance,
    wxInt64* pPixelDist, double* pPinValue)
{
    const wxInt64 posS = viewInfo.TimeToPosition(selend);
    const wxInt64 pos0 = viewInfo.TimeToPosition(t0);
    wxInt64 pixelDist = std::abs(posS - pos0);
    bool chooseLeft = true;

    if (t1 <= t0) {
        // Special case when selection is a point, and thus left
        // and right distances are the same
        chooseLeft = (selend < t0);
    } else {
        const wxInt64 pos1 = viewInfo.TimeToPosition(t1);
        const wxInt64 rightDist = std::abs(posS - pos1);
        if (rightDist < pixelDist) {
            chooseLeft = false, pixelDist = rightDist;
        }
    }

    SetIfNotNull(pPixelDist, pixelDist);

    if (onlyWithinSnapDistance
        && pixelDist >= SELECTION_RESIZE_REGION) {
        SetIfNotNull(pPinValue, -1.0);
        return SBNone;
    } else if (chooseLeft) {
        SetIfNotNull(pPinValue, t1);
        return SBLeft;
    } else {
        SetIfNotNull(pPinValue, t0);
        return SBRight;
    }
}

SelectionBoundary ChooseBoundary(
    const ViewInfo& viewInfo,
    wxCoord xx, wxCoord yy, const ChannelView& channelView,
    const wxRect& rect,
    bool mayDragWidth, bool onlyWithinSnapDistance,
    double* pPinValue = NULL)
{
    // Choose one of four boundaries to adjust, or the center frequency.
    // May choose frequencies only if in a spectrogram view and
    // within the time boundaries.
    // May choose no boundary if onlyWithinSnapDistance is true.
    // Otherwise choose the eligible boundary nearest the mouse click.
    const double selend = viewInfo.PositionToTime(xx, rect.x);
    wxInt64 pixelDist = 0;
    const double t0 = viewInfo.selectedRegion.t0();
    const double t1 = viewInfo.selectedRegion.t1();

    SelectionBoundary boundary
        =ChooseTimeBoundary(t0, t1, viewInfo, selend, onlyWithinSnapDistance,
                            &pixelDist, pPinValue);

    //const double t0 = viewInfo.selectedRegion.t0();
    //const double t1 = viewInfo.selectedRegion.t1();
    const double f0 = viewInfo.selectedRegion.f0();
    const double f1 = viewInfo.selectedRegion.f1();
    const double fc = viewInfo.selectedRegion.fc();
    double ratio = 0;

    bool chooseTime = true;
    bool chooseBottom = true;
    bool chooseCenter = false;
    // Consider adjustment of frequencies only if mouse is
    // within the time boundaries
    if (!viewInfo.selectedRegion.isPoint()
        && t0 <= selend && selend < t1
        && isSpectralSelectionView(channelView)) {
        auto pWc = channelView.FindChannel<const WaveChannel>();
        // Spectral selection track is always wave
        assert(pWc);
        auto& wc = *pWc;
        const wxInt64 bottomSel = (f0 >= 0)
                                  ? FrequencyToPosition(wc, f0, rect.y, rect.height)
                                  : rect.y + rect.height;
        const wxInt64 topSel = (f1 >= 0)
                               ? FrequencyToPosition(wc, f1, rect.y, rect.height)
                               : rect.y;
        wxInt64 signedBottomDist = (int)(yy - bottomSel);
        wxInt64 verticalDist = std::abs(signedBottomDist);
        if (bottomSel == topSel) {
            // Top and bottom are too close to resolve on screen
            chooseBottom = (signedBottomDist >= 0);
        } else {
            const wxInt64 topDist = std::abs((int)(yy - topSel));
            if (topDist < verticalDist) {
                chooseBottom = false, verticalDist = topDist;
            }
        }
        if (fc > 0
#ifdef SPECTRAL_EDITING_ESC_KEY
            && mayDragWidth
#endif
            ) {
            const wxInt64 centerSel
                =FrequencyToPosition(wc, fc, rect.y, rect.height);
            const wxInt64 centerDist = abs((int)(yy - centerSel));
            if (centerDist < verticalDist) {
                chooseCenter = true, verticalDist = centerDist,
                ratio = f1 / fc;
            }
        }
        if (verticalDist >= 0
            && verticalDist < pixelDist) {
            pixelDist = verticalDist;
            chooseTime = false;
        }
    }

    if (!chooseTime) {
        // PRL:  Seems I need a larger tolerance to make snapping work
        // at top of track, not sure why
        if (onlyWithinSnapDistance
            && pixelDist >= FREQ_SNAP_DISTANCE) {
            SetIfNotNull(pPinValue, -1.0);
            return SBNone;
        } else if (chooseCenter) {
            SetIfNotNull(pPinValue, ratio);
            return SBCenter;
        } else if (mayDragWidth && fc > 0) {
            SetIfNotNull(pPinValue, fc);
            return SBWidth;
        } else if (chooseBottom) {
            SetIfNotNull(pPinValue, f1);
            return SBBottom;
        } else {
            SetIfNotNull(pPinValue, f0);
            return SBTop;
        }
    } else {
        return boundary;
    }
}

wxCursor* SelectCursor()
{
    static auto selectCursor
        =::MakeCursor(wxCURSOR_IBEAM, IBeamCursorXpm, 17, 16);
    return &*selectCursor;
}

wxCursor* EnvelopeCursor()
{
    // This one doubles as the center frequency cursor for spectral selection:
    static auto envelopeCursor
        =::MakeCursor(wxCURSOR_ARROW, EnvCursorXpm, 16, 16);
    return &*envelopeCursor;
}

void SetTipAndCursorForBoundary
    (SelectionBoundary boundary, bool frequencySnapping,
    TranslatableString& tip, wxCursor*& pCursor)
{
    static auto adjustLeftSelectionCursor
        =::MakeCursor(wxCURSOR_POINT_LEFT, SelectionLeftXpm, 16, 16);
    static auto adjustRightSelectionCursor
        =::MakeCursor(wxCURSOR_POINT_RIGHT, SelectionRightXpm, 16, 16);
    static auto bottomFrequencyCursor
        =::MakeCursor(wxCURSOR_ARROW, BottomFrequencyCursorXpm, 16, 16);
    static auto topFrequencyCursor
        =::MakeCursor(wxCURSOR_ARROW, TopFrequencyCursorXpm, 16, 16);
    static auto bandWidthCursor
        =::MakeCursor(wxCURSOR_ARROW, BandWidthCursorXpm, 16, 16);

    switch (boundary) {
    case SBNone:
        pCursor = SelectCursor();
        break;
    case SBLeft:
        tip = XO("Click and drag to move left selection boundary.");
        pCursor = &*adjustLeftSelectionCursor;
        break;
    case SBRight:
        tip = XO("Click and drag to move right selection boundary.");
        pCursor = &*adjustRightSelectionCursor;
        break;
    case SBBottom:
        tip = XO("Click and drag to move bottom selection frequency.");
        pCursor = &*bottomFrequencyCursor;
        break;
    case SBTop:
        tip = XO("Click and drag to move top selection frequency.");
        pCursor = &*topFrequencyCursor;
        break;
    case SBCenter:
    {
#ifndef SPECTRAL_EDITING_ESC_KEY
        tip
            =frequencySnapping
              ? XO("Click and drag to move center selection frequency to a spectral peak.")
              : XO("Click and drag to move center selection frequency.");

#else
        shiftDown;

        tip
            =XO("Click and drag to move center selection frequency.");

#endif

        pCursor = EnvelopeCursor();
    }
    break;
    case SBWidth:
        tip = XO("Click and drag to adjust frequency bandwidth.");
        pCursor = &*bandWidthCursor;
        break;
    default:
        wxASSERT(false);
    }   // switch
        // Falls through the switch if there was no boundary found.
}
}

UIHandlePtr SelectHandle::HitTest
    (std::weak_ptr<SelectHandle>& holder,
    const TrackPanelMouseState& st, const AudacityProject* pProject,
    const std::shared_ptr<ChannelView>& pChannelView)
{
    // This handle is a little special because there may be some state to
    // preserve during movement before the click.
    auto old = holder.lock();
    bool oldUseSnap = true;
    if (old) {
        // It should not have started listening to timer events
        if (old->mTimerHandler) {
            wxASSERT(false);
            // Handle this eventuality anyway, don't leave a dangling back-pointer
            // in the attached event handler.
            old->mTimerHandler.reset();
        }
        oldUseSnap = old->mUseSnap;
    }

    const auto& viewInfo = ViewInfo::Get(*pProject);
    auto result = std::make_shared<SelectHandle>(
        pChannelView, oldUseSnap, TrackList::Get(*pProject), st, viewInfo);

    result = AssignUIHandlePtr(holder, result);

    //Make sure we are within the selected track
    auto pTrack = FindTrack(pChannelView->FindChannel().get());
    if (!pTrack->GetSelected()) {
        return result;
    }

    {
        const wxRect& rect = st.rect;
        wxInt64 leftSel = viewInfo.TimeToPosition(viewInfo.selectedRegion.t0(), rect.x);
        wxInt64 rightSel = viewInfo.TimeToPosition(viewInfo.selectedRegion.t1(), rect.x);
        // Something is wrong if right edge comes before left edge
        wxASSERT(!(rightSel < leftSel));
        static_cast<void>(leftSel); // Suppress unused variable warnings if not in debug-mode
        static_cast<void>(rightSel);
    }

    return result;
}

UIHandle::Result SelectHandle::NeedChangeHighlight
    (const SelectHandle& oldState, const SelectHandle& newState)
{
    auto useSnap = oldState.mUseSnap;
    // This is guaranteed when constructing the NEW handle:
    wxASSERT(useSnap == newState.mUseSnap);
    if (!useSnap) {
        return 0;
    }

    auto& oldSnapState = oldState.mSnapStart;
    auto& newSnapState = newState.mSnapStart;
    if (oldSnapState.Snapped() == newSnapState.Snapped()
        && (!oldSnapState.Snapped()
            || oldSnapState.outCoord == newSnapState.outCoord)) {
        return 0;
    }

    return RefreshCode::RefreshAll;
}

SelectHandle::SelectHandle(
    const std::shared_ptr<ChannelView>& pChannelView, bool useSnap,
    const TrackList& trackList,
    const TrackPanelMouseState& st, const ViewInfo& viewInfo)
    : mpView{pChannelView}
    // Selection dragging can snap to play region boundaries
    , mSnapManager{std::make_shared<SnapManager>(
                       *trackList.GetOwner(), trackList, viewInfo, SnapPointArray {
        SnapPoint { viewInfo.playRegion.GetLastActiveStart() },
        SnapPoint { viewInfo.playRegion.GetLastActiveEnd() },
    })}
{
    const wxMouseState& state = st.state;
    mRect = st.rect;

    auto time = std::max(0.0, viewInfo.PositionToTime(state.m_x, mRect.x));
    auto pTrack = FindTrack(pChannelView->FindChannel().get());
    mSnapStart = mSnapManager->Snap(pTrack, time, false);
    if (mSnapStart.snappedPoint) {
        mSnapStart.outCoord += mRect.x;
    } else {
        mSnapStart.outCoord = -1;
    }

    mUseSnap = useSnap;
}

SelectHandle::~SelectHandle()
{
}

std::shared_ptr<Channel> SelectHandle::FindChannel()
{
    if (const auto pView = mpView.lock()) {
        return pView->FindChannel();
    } else {
        return {}
    }
}

std::shared_ptr<const Track> SelectHandle::FindTrack() const
{
    return TrackFromChannel(const_cast<SelectHandle&>(*this).FindChannel());
}

Track* SelectHandle::FindTrack(Channel* pChannel)
{
    return pChannel
           ? dynamic_cast<Track*>(&pChannel->GetChannelGroup())
           : nullptr;
}

Track* SelectHandle::FindTrack()
{
    const auto pChannel = FindChannel();
    return FindTrack(pChannel.get());
}

namespace {
// Is the distance between A and B less than D?
template< class A, class B, class DIST > bool within(A a, B b, DIST d)
{
    return (a > b - d) && (a < b + d);
}

inline double findMaxRatio(double center, double rate)
{
    const double minFrequency = 1.0;
    const double maxFrequency = (rate / 2.0);
    const double frequency
        =std::min(maxFrequency,
                  std::max(minFrequency, center));
    return
        std::min(frequency / minFrequency, maxFrequency / frequency);
}
}

void SelectHandle::Enter(bool, AudacityProject* project)
{
    SetUseSnap(true, project);
}

void SelectHandle::SetUseSnap(bool use, AudacityProject* project)
{
    mUseSnap = use;

    bool hasSnap = HasSnap();
    if (hasSnap) {
        // Repaint to turn the snap lines on or off
        mChangeHighlight = RefreshCode::RefreshAll;
    }

    if (IsDragging()) {
        // Readjust the moving selection end
        AssignSelection(
            ViewInfo::Get(*project),
            mUseSnap ? mSnapEnd.outTime : mSnapEnd.timeSnappedTime);
    }
}

bool SelectHandle::HasSnap() const
{
    return
        (IsDragging() ? mSnapEnd : mSnapStart).snappedPoint;
}

bool SelectHandle::HasEscape(AudacityProject*) const
{
    return HasSnap() && mUseSnap;
}

bool SelectHandle::Escape(AudacityProject* project)
{
    if (SelectHandle::HasEscape(project)) {
        SetUseSnap(false, project);
        return true;
    }
    return false;
}

UIHandle::Result SelectHandle::Click(
    const TrackPanelMouseEvent& evt, AudacityProject* pProject)
{
    /// This method gets called when we're handling selection
    /// and the mouse was just clicked.

    using namespace RefreshCode;

    const auto pView = mpView.lock();
    if (!pView) {
        return Cancelled;
    }
    auto& view = *pView;

    wxMouseEvent& event = evt.event;
    auto& trackList = TrackList::Get(*pProject);
    const auto pTrack = FindTrack();
    if (!pTrack) {
        return Cancelled;
    }
    auto& trackPanel = TrackPanel::Get(*pProject);
    auto& viewInfo = ViewInfo::Get(*pProject);

    mMostRecentX = event.m_x;
    mMostRecentY = event.m_y;

    bool selectChange = (
        event.LeftDown()
        && event.ControlDown()
        && pTrack->TypeSwitch<bool>([&](LabelTrack&){
        // We should reach this, only in default of other hits on glyphs or
        // text boxes.
        bool bShift = event.ShiftDown();
        bool unsafe = ProjectAudioIO::Get(*pProject).IsAudioActive();
        SelectUtilities::DoListSelection(
            *pProject, *pTrack, bShift, true, !unsafe);
        return true;
    })
        );
    if (selectChange) {
        // Do not start a drag
        return RefreshAll | Cancelled;
    }

    auto& selectionState = SelectionState::Get(*pProject);
    if (event.LeftDClick() && !event.ShiftDown()) {
        // Deselect all other tracks and select this one.
        selectionState.SelectNone(trackList);

        if (pTrack) {
            selectionState.SelectTrack(*pTrack, true, true);
        }

        // Default behavior: select whole track
        SelectionState::SelectTrackLength(
            viewInfo, *pTrack, SyncLockState::Get(*pProject).IsSyncLocked());

        // Special case: if we're over a clip in a WaveTrack,
        // select just that clip
        if (const auto pWc = view.FindChannel<WaveChannel>()) {
            auto& wc = *pWc;
            auto time = viewInfo.PositionToTime(event.m_x, mRect.x);
            const auto selectedClip
                =WaveChannelUtilities::GetIntervalAtTime(wc, time);
            if (selectedClip) {
                viewInfo.selectedRegion.setTimes(
                    selectedClip->GetPlayStartTime(), selectedClip->GetPlayEndTime());
            }
        }

        ProjectHistory::Get(*pProject).ModifyState(false);

        // Do not start a drag
        return RefreshAll | Cancelled;
    } else if (!event.LeftDown()) {
        return Cancelled;
    }

    mInitialSelection = viewInfo.selectedRegion;

    mSelectionStateChanger
        =std::make_shared<SelectionStateChanger>(selectionState, trackList);

    mSelectionBoundary = 0;

    bool bShiftDown = event.ShiftDown();
    bool bCtrlDown = event.ControlDown();

    mSelStart = mUseSnap ? mSnapStart.outTime : mSnapStart.timeSnappedTime;
    auto xx = viewInfo.TimeToPosition(mSelStart, mRect.x);

    // I. Shift-click adjusts an existing selection
    if (bShiftDown || bCtrlDown) {
        if (bShiftDown) {
            // Selection state pertains to tracks, not channels
            selectionState.ChangeSelectionOnShiftClick(trackList, *pTrack);
        }
        if (bCtrlDown) {
            //Commented out bIsSelected toggles, as in Track Control Panel.
            //bool bIsSelected = pTrack->GetSelected();
            //Actual bIsSelected will always add.
            bool bIsSelected = false;
            // Don't toggle away the last selected track.
            if (!bIsSelected || trackPanel.GetSelectedTrackCount() > 1) {
                if (pTrack) {
                    selectionState.SelectTrack(*pTrack, !bIsSelected, true);
                }
            }
        }

        double value;
        // Shift-click, choose closest boundary
        SelectionBoundary boundary
            =ChooseBoundary(viewInfo, xx, event.m_y,
                            view, mRect, false, false, &value);
        mSelectionBoundary = boundary;
        switch (boundary) {
        case SBLeft:
        case SBRight:
        {
            // If drag starts, change time selection only
            // (also exit frequency snapping)
            mFreqSelMode = FREQ_SEL_INVALID;
            mSelStartValid = true;
            mSelStart = value;
            mSnapStart = SnapResults{};
            AdjustSelection(pProject, viewInfo, event.m_x, mRect.x, pTrack);
            break;
        }
        case SBBottom:
        case SBTop:
        {
            auto pWc = view.FindChannel<const WaveChannel>();
            // Spectral selection track is always wave
            assert(pWc);
            mFreqSelTrack = pWc;
            mFreqSelPin = value;
            mFreqSelMode
                =(boundary == SBBottom)
                  ? FREQ_SEL_BOTTOM_FREE : FREQ_SEL_TOP_FREE;

            // Drag frequency only, not time:
            mSelStartValid = false;
            AdjustFreqSelection(*pWc,
                                viewInfo, event.m_y, mRect.y, mRect.height);
            break;
        }
        case SBCenter:
        {
            auto pWc = view.FindChannel<const WaveChannel>();
            // Spectral selection track is always wave
            assert(pWc);
            HandleCenterFrequencyClick(viewInfo, true, pWc, value);
            break;
        }
        default:
            wxASSERT(false);
        }

        // For persistence of the selection change:
        ProjectHistory::Get(*pProject).ModifyState(false);

        // Get timer events so we can auto-scroll
        Connect(pProject);

        // Full refresh since the label area may need to indicate
        // newly selected tracks.
        return RefreshAll;
    }

    // II. Unmodified click starts a NEW selection

    //Make sure you are within the selected track
    bool startNewSelection = true;
    if (pTrack && pTrack->GetSelected()) {
        if (mFreqSelMode == FREQ_SEL_SNAPPING_CENTER
            && isSpectralSelectionView(view)) {
            // This code is no longer reachable, but it had a place in the
            // spectral selection prototype.  It used to be that you could be
            // in a center-frequency-snapping mode that was not a mouse drag
            // but responded to mouse movements.  Click exited that and dragged
            // width instead.  PRL.

            // Ignore whether we are inside the time selection.
            // Exit center-snapping, start dragging the width.
            mFreqSelMode = FREQ_SEL_PINNED_CENTER;
            auto pWc = view.FindChannel<const WaveChannel>();
            // Spectral selection track is always wave
            assert(pWc);
            mFreqSelTrack = pWc;
            mFreqSelPin = viewInfo.selectedRegion.fc();
            // Do not adjust time boundaries
            mSelStartValid = false;
            AdjustFreqSelection(*pWc,
                                viewInfo, event.m_y, mRect.y, mRect.height);
            // For persistence of the selection change:
            ProjectHistory::Get(*pProject).ModifyState(false);
            mSelectionBoundary = SBWidth;
            return RefreshNone;
        } else {
            // Not shift-down, choose boundary only within snapping
            double value;
            SelectionBoundary boundary
                =ChooseBoundary(viewInfo, xx, event.m_y,
                                view, mRect, true, true, &value);
            mSelectionBoundary = boundary;
            switch (boundary) {
            case SBNone:
                // startNewSelection remains true
                break;
            case SBLeft:
            case SBRight:
                startNewSelection = false;
                // Disable frequency selection
                mFreqSelMode = FREQ_SEL_INVALID;
                mSelStartValid = true;
                mSelStart = value;
                mSnapStart = SnapResults{};
                break;
            case SBBottom:
            case SBTop:
            case SBWidth: {
                startNewSelection = false;
                // Disable time selection
                mSelStartValid = false;
                auto pWc = view.FindChannel<const WaveChannel>();
                // Spectral selection track is always wave
                assert(pWc);
                mFreqSelTrack = pWc;
                mFreqSelPin = value;
                mFreqSelMode
                    =(boundary == SBWidth) ? FREQ_SEL_PINNED_CENTER
                      : (boundary == SBBottom) ? FREQ_SEL_BOTTOM_FREE
                      : FREQ_SEL_TOP_FREE;
                break;
            }
            case SBCenter:
            {
                auto pWc = view.FindChannel<const WaveChannel>();
                // Spectral selection track is always wave
                assert(pWc);
                mFreqSelTrack = pWc;
                HandleCenterFrequencyClick(viewInfo, false, pWc, value);
                startNewSelection = false;
                break;
            }
            default:
                wxASSERT(false);
            }
        }
    }

    // III. Common case for starting a NEW selection

    if (startNewSelection) {
        // If we didn't move a selection boundary, start a NEW selection
        selectionState.SelectNone(trackList);
        StartFreqSelection(viewInfo, event.m_y, mRect.y, mRect.height,
                           view);
        StartSelection(pProject);
        if (pTrack) {
            selectionState.SelectTrack(*pTrack, true, true);
        }
        TrackFocus::Get(*pProject).Set(pTrack);

        Connect(pProject);
        return RefreshAll;
    } else {
        Connect(pProject);
        return RefreshAll;
    }
}

UIHandle::Result SelectHandle::Drag(const TrackPanelMouseEvent& evt,
                                    AudacityProject* pProject)
{
    using namespace RefreshCode;

    const auto pView = mpView.lock();
    if (!pView) {
        return Cancelled;
    }
    auto& view = *pView;

    auto& viewInfo = ViewInfo::Get(*pProject);
    const wxMouseEvent& event = evt.event;

    int x = mAutoScrolling ? mMostRecentX : event.m_x;
    int y = mAutoScrolling ? mMostRecentY : event.m_y;
    mMostRecentX = x;
    mMostRecentY = y;

    /// AS: If we're dragging to adjust a selection (or actually,
    ///  if the screen is scrolling while you're selecting), we
    ///  handle it here.

    // Fuhggeddaboudit if we're not dragging and not autoscrolling.
    if (!event.Dragging() && !mAutoScrolling) {
        return RefreshNone;
    }

    if (event.CmdDown()) {
        // Ctrl-drag has no meaning, fuhggeddaboudit
        // JKC YES it has meaning.
        //return RefreshNone;
    }

    // Also fuhggeddaboudit if not in a track.
    const auto pChannel = view.FindChannel();
    if (!pChannel) {
        return RefreshNone;
    }

    // JKC: Logic to prevent a selection smaller than 5 pixels to
    // prevent accidental dragging when selecting.
    // (if user really wants a tiny selection, they should zoom in).
    // Can someone make this value of '5' configurable in
    // preferences?
    enum {
        minimumSizedSelection = 5
    };                                 //measured in pixels

    // Might be dragging frequency bounds only, test
    if (mSelStartValid) {
        wxInt64 SelStart = viewInfo.TimeToPosition(mSelStart, mRect.x); //cvt time to pixels.
        // Abandon this drag if selecting < 5 pixels.
        if (wxLongLong(SelStart - x).Abs() < minimumSizedSelection) {
            return RefreshNone;
        }
    }

    if (evt.pCell) {
        if (auto clickedTrack
                =static_cast<CommonTrackPanelCell*>(evt.pCell.get())->FindTrack()) {
            // Handle which tracks are selected
            Track* sTrack = FindTrack();
            Track* eTrack = clickedTrack.get();
            auto& trackList = TrackList::Get(*pProject);
            if (sTrack && eTrack && !event.ControlDown()) {
                auto& selectionState = SelectionState::Get(*pProject);
                selectionState.SelectRangeOfTracks(trackList, *sTrack, *eTrack);
            }

   #ifndef SPECTRAL_EDITING_ESC_KEY
            if (mFreqSelMode == FREQ_SEL_SNAPPING_CENTER
                && !viewInfo.selectedRegion.isPoint()) {
                MoveSnappingFreqSelection(pProject, viewInfo, y,
                                          mRect.y, mRect.height, view, sTrack);
            } else
   #endif
            if (const auto pWaveChannel = mFreqSelTrack.lock()
                ; pWaveChannel == pChannel
                ) {
                AdjustFreqSelection(*pWaveChannel,
                                    viewInfo, y, mRect.y, mRect.height);
            }

            AdjustSelection(pProject, viewInfo, x, mRect.x, clickedTrack.get());
        }
    }

    return RefreshNone

           // If scrubbing does not use the helper poller thread, then
           // don't refresh at every mouse event, because it slows down seek-scrub.
           // Instead, let OnTimer do it, which is often enough.
           // And even if scrubbing does use the thread, then skipping refresh does not
           // bring that advantage, but it is probably still a good idea anyway.

           // | UpdateSelection

    ;
}

HitTestPreview SelectHandle::Preview
    (const TrackPanelMouseState& st, AudacityProject* pProject)
{
    if (!HasSnap() && !mUseSnap) {
        // Moved out of snapping; revert to un-escaped state
        mUseSnap = true;
    }

    const auto pView = mpView.lock();
    if (!pView) {
        return {}
    }
    auto& view = *pView;

    const auto pChannel = view.FindChannel();
    if (!pChannel) {
        return {}
    }

    TranslatableString tip;
    wxCursor* pCursor = SelectCursor();
    if (IsDragging()) {
        // Use same cursor as at the click
        SetTipAndCursorForBoundary
            (SelectionBoundary(mSelectionBoundary),
            (mFreqSelMode == FREQ_SEL_SNAPPING_CENTER),
            tip, pCursor);
    } else {
        // Choose one of many cursors for mouse-over

        auto& viewInfo = ViewInfo::Get(*pProject);

        auto& state = st.state;
        auto time = mUseSnap ? mSnapStart.outTime : mSnapStart.timeSnappedTime;
        auto xx = viewInfo.TimeToPosition(time, mRect.x);

        const bool bMultiToolMode
            =(ToolCodes::multiTool == ProjectSettings::Get(*pProject).GetTool());

        //In Multi-tool mode, give multitool prompt if no-special-hit.
        const auto pTrack = FindTrack();
        if (bMultiToolMode) {
            // Look up the current key binding for Preferences.
            // (Don't assume it's the default!)
            auto keyStr
                =CommandManager::Get(*pProject).GetKeyFromName(wxT("Preferences"))
                  .Display(true);
            if (keyStr.empty()) {
                // No keyboard preference defined for opening Preferences dialog
                /* i18n-hint: These are the names of a menu and a command in that menu */
                keyStr = _("Edit, Preferences...");
            }

            /* i18n-hint: %s is usually replaced by "Ctrl+P" for Windows/Linux, "Command+," for Mac */
            tip = XO("Multi-Tool Mode: %s for Mouse and Keyboard Preferences.")
                  .Format(keyStr);
            // Later in this function we may point to some other string instead.
            if (!pTrack->GetSelected()) {
            } else {
                const wxRect& rect = st.rect;
                const bool bShiftDown = state.ShiftDown();
                const bool bCtrlDown = state.ControlDown();
                const bool bModifierDown = bShiftDown || bCtrlDown;

                // If not shift-down and not snapping center, then
                // choose boundaries only in snapping tolerance,
                // and may choose center.
                SelectionBoundary boundary
                    =ChooseBoundary(viewInfo, xx, state.m_y,
                                    view, rect, !bModifierDown, !bModifierDown);

                SetTipAndCursorForBoundary(boundary, !bShiftDown, tip, pCursor);
            }
        }

#if 0
        // This is a vestige of an idea in the prototype version.
        // Center would snap without mouse button down, click would pin the center
        // and drag width.
        if ((mFreqSelMode == FREQ_SEL_SNAPPING_CENTER)
            && isSpectralSelectionView(view)) {
            // Not shift-down, but center frequency snapping toggle is on
            tip = XO("Click and drag to set frequency bandwidth.");
            pCursor = &*envelopeCursor;
            return {};
        }
#endif

        if (!pTrack->GetSelected()) {
        } else {
            const wxRect& rect = st.rect;
            const bool bShiftDown = state.ShiftDown();
            const bool bCtrlDown = state.ControlDown();
            const bool bModifierDown = bShiftDown || bCtrlDown;
            SelectionBoundary boundary = ChooseBoundary(
                viewInfo, xx, state.m_y,
                view, rect, !bModifierDown, !bModifierDown);
            SetTipAndCursorForBoundary(boundary, !bShiftDown, tip, pCursor);
        }
    }
    if (tip.empty()) {
        tip = XO("Click and drag to select audio");
    }
    if (HasEscape(pProject) && mUseSnap) {
        tip.Join(
/* i18n-hint: "Snapping" means automatic alignment of selection edges to any nearby label or clip boundaries */
            XO("(snapping)"), wxT(" ")
            );
    }
    return { tip, pCursor };
}

UIHandle::Result SelectHandle::Release(
    const TrackPanelMouseEvent& event, AudacityProject* pProject, wxWindow*)
{
    PitchAndSpeedDialog::Get(*pProject).TryRetarget(event);
    using namespace RefreshCode;
    ProjectHistory::Get(*pProject).ModifyState(false);
    mFrequencySnapper.reset();
    mSnapManager.reset();
    if (mSelectionStateChanger) {
        mSelectionStateChanger->Commit();
        mSelectionStateChanger.reset();
    }

    if (mUseSnap && (mSnapStart.outCoord != -1 || mSnapEnd.outCoord != -1)) {
        return RefreshAll;
    } else {
        return RefreshNone;
    }
}

UIHandle::Result SelectHandle::Cancel(AudacityProject* pProject)
{
    mSelectionStateChanger.reset();
    ViewInfo::Get(*pProject).selectedRegion = mInitialSelection;

    return RefreshCode::RefreshAll;
}

void SelectHandle::Draw(
    TrackPanelDrawingContext& context,
    const wxRect& rect, unsigned iPass)
{
    if (iPass == TrackArtist::PassSnapping) {
        auto& dc = context.dc;
        // Draw snap guidelines if we have any
        if (mSnapManager) {
            auto coord1 = (mUseSnap || IsDragging()) ? mSnapStart.outCoord : -1;
            auto coord2 = (!mUseSnap || !IsDragging()) ? -1 : mSnapEnd.outCoord;
            TrackArt::DrawSnapLines(&dc, coord1, coord2);
        }
    }
}

wxRect SelectHandle::DrawingArea(
    TrackPanelDrawingContext&,
    const wxRect& rect, const wxRect& panelRect, unsigned iPass)
{
    if (iPass == TrackArtist::PassSnapping) {
        return MaximizeHeight(rect, panelRect);
    } else {
        return rect;
    }
}

void SelectHandle::Connect(AudacityProject* pProject)
{
    mTimerHandler = std::make_shared<TimerHandler>(this, pProject);
}

class SelectHandle::TimerHandler
{
public:
    TimerHandler(SelectHandle* pParent, AudacityProject* pProject)
        : mParent{pParent}
        , mConnectedProject{pProject}
    {
        if (mConnectedProject) {
            mSubscription = ProjectWindow::Get(*mConnectedProject)
                            .GetPlaybackScroller().Subscribe(*this, &SelectHandle::TimerHandler::OnTimer);
        }
    }

    // Receives timer event notifications, to implement auto-scroll
    void OnTimer(Observer::Message);

private:
    SelectHandle* mParent;
    AudacityProject* mConnectedProject;
    Observer::Subscription mSubscription;
};

void SelectHandle::TimerHandler::OnTimer(Observer::Message)
{
    // AS: If the user is dragging the mouse and there is a track that
    //  has captured the mouse, then scroll the screen, as necessary.

    ///  We check on each timer tick to see if we need to scroll.

    // DM: If we're "autoscrolling" (which means that we're scrolling
    //  because the user dragged from inside to outside the window,
    //  not because the user clicked in the scroll bar), then
    //  the selection code needs to be handled slightly differently.
    //  We set this flag ("mAutoScrolling") to tell the selecting
    //  code that we didn't get here as a result of a mouse event,
    //  and therefore it should ignore the event,
    //  and instead use the last known mouse position.  Setting
    //  this flag also causes the Mac to redraw immediately rather
    //  than waiting for the next update event; this makes scrolling
    //  smoother on MacOS 9.

    const auto project = mConnectedProject;
    const auto& trackPanel = TrackPanel::Get(*project);
    auto& viewport = Viewport::Get(*project);
    if (mParent->mMostRecentX >= mParent->mRect.x + mParent->mRect.width) {
        mParent->mAutoScrolling = true;
        viewport.OnScrollRight();
    } else if (mParent->mMostRecentX < mParent->mRect.x) {
        mParent->mAutoScrolling = true;
        viewport.OnScrollLeft();
    } else {
        // Bug1387:  enable autoscroll during drag, if the pointer is at either
        // extreme x coordinate of the screen, even if that is still within the
        // track area.

        int xx = mParent->mMostRecentX, yy = 0;
        trackPanel.ClientToScreen(&xx, &yy);
        if (xx == 0) {
            mParent->mAutoScrolling = true;
            viewport.OnScrollLeft();
        } else {
            int width, height;
            ::wxDisplaySize(&width, &height);
            if (xx == width - 1) {
                mParent->mAutoScrolling = true;
                viewport.OnScrollRight();
            }
        }
    }

    const auto pChannel = mParent->FindChannel();
    if (mParent->mAutoScrolling && pChannel) {
        // AS: To keep the selection working properly as we scroll,
        //  we fake a mouse event (remember, this method is called
        //  from a timer tick).

        // AS: For some reason, GCC won't let us pass this directly.
        wxMouseEvent evt(wxEVT_MOTION);
        const auto size = trackPanel.GetSize();
        mParent->Drag(
            TrackPanelMouseEvent {
            evt, mParent->mRect, size,
            ChannelView::Get(*pChannel).shared_from_this() },
            project
            );
        mParent->mAutoScrolling = false;
        TrackPanel::Get(*mConnectedProject).Refresh(false);
    }
}

/// Reset our selection markers.
void SelectHandle::StartSelection(AudacityProject* pProject)
{
    auto& viewInfo = ViewInfo::Get(*pProject);
    mSelStartValid = true;

    viewInfo.selectedRegion.setTimes(mSelStart, mSelStart);

    // PRL:  commented out the Sonify stuff with the TrackPanel refactor.
    // It was no-op anyway.
    //SonifyBeginModifyState();
    ProjectHistory::Get(*pProject).ModifyState(false);
    //SonifyEndModifyState();
}

/// Extend or contract the existing selection
void SelectHandle::AdjustSelection(AudacityProject* pProject,
                                   ViewInfo& viewInfo, int mouseXCoordinate, int trackLeftEdge,
                                   Track* track)
{
    if (!mSelStartValid) {
        // Must be dragging frequency bounds only.
        return;
    }

    double selend
        =std::max(0.0, viewInfo.PositionToTime(mouseXCoordinate, trackLeftEdge));
    double origSelend = selend;

    if (!track) {
        const auto sChannel = FindChannel();
        track = sChannel
                ? dynamic_cast<Track*>(&sChannel->GetChannelGroup())
                : nullptr;
    }

    if (track && mSnapManager.get()) {
        bool rightEdge = (selend > mSelStart);
        mSnapEnd = mSnapManager->Snap(track, selend, rightEdge);
        if (mSnapEnd.Snapped()) {
            if (mUseSnap) {
                selend = mSnapEnd.outTime;
            }
            if (mSnapEnd.snappedPoint) {
                mSnapEnd.outCoord += trackLeftEdge;
            }
        }
        if (!mSnapEnd.snappedPoint) {
            mSnapEnd.outCoord = -1;
        }

        // Check if selection endpoints are too close together to snap (unless
        // using snap-to-time -- then we always accept the snap results)
        if (mSnapStart.outCoord >= 0
            && mSnapEnd.outCoord >= 0
            && std::abs(mSnapStart.outCoord - mSnapEnd.outCoord) < 3) {
            if (!mSnapEnd.snappedTime) {
                selend = origSelend;
            }
            mSnapEnd.outCoord = -1;
        }
    }
    AssignSelection(viewInfo, selend);
}

void SelectHandle::AssignSelection(ViewInfo& viewInfo, double selend)
{
    double sel0, sel1;
    if (mSelStart < selend) {
        sel0 = mSelStart;
        sel1 = selend;
    } else {
        sel1 = mSelStart;
        sel0 = selend;
    }

    viewInfo.selectedRegion.setTimes(sel0, sel1);
}

void SelectHandle::StartFreqSelection(ViewInfo& viewInfo,
                                      int mouseYCoordinate, int trackTopEdge,
                                      int trackHeight, ChannelView& channelView)
{
    mFreqSelTrack.reset();
    mFreqSelMode = FREQ_SEL_INVALID;
    mFreqSelPin = SelectedRegion::UndefinedFrequency;

    if (isSpectralSelectionView(channelView)) {
        // Spectral selection track is always wave
        auto shTrack = channelView.FindChannel<WaveChannel>();
        mFreqSelTrack = shTrack;
        mFreqSelMode = FREQ_SEL_FREE;
        mFreqSelPin
            =PositionToFrequency(*shTrack, false, mouseYCoordinate,
                                 trackTopEdge, trackHeight);
        viewInfo.selectedRegion.setFrequencies(mFreqSelPin, mFreqSelPin);
    }
}

void SelectHandle::AdjustFreqSelection(
    const WaveChannel& wc, ViewInfo& viewInfo,
    int mouseYCoordinate, int trackTopEdge,
    int trackHeight)
{
    if (mFreqSelMode == FREQ_SEL_INVALID
        || mFreqSelMode == FREQ_SEL_SNAPPING_CENTER) {
        return;
    }

    // Extension happens only when dragging in the same track in which we
    // started, and that is of a spectrogram display type.

    const double rate =  wc.GetRate();
    const double frequency
        =PositionToFrequency(wc, true, mouseYCoordinate,
                             trackTopEdge, trackHeight);

    // Dragging center?
    if (mFreqSelMode == FREQ_SEL_DRAG_CENTER) {
        if (frequency == rate || frequency < 1.0) {
            // snapped to top or bottom
            viewInfo.selectedRegion.setFrequencies(
                SelectedRegion::UndefinedFrequency,
                SelectedRegion::UndefinedFrequency);
        } else {
            // mFreqSelPin holds the ratio of top to center
            const double maxRatio = findMaxRatio(frequency, rate);
            const double ratio = std::min(maxRatio, mFreqSelPin);
            viewInfo.selectedRegion.setFrequencies(
                frequency / ratio, frequency * ratio);
        }
    } else if (mFreqSelMode == FREQ_SEL_PINNED_CENTER) {
        if (mFreqSelPin >= 0) {
            // Change both upper and lower edges leaving centre where it is.
            if (frequency == rate || frequency < 1.0) {
                // snapped to top or bottom
                viewInfo.selectedRegion.setFrequencies(
                    SelectedRegion::UndefinedFrequency,
                    SelectedRegion::UndefinedFrequency);
            } else {
                // Given center and mouse position, find ratio of the larger to the
                // smaller, limit that to the frequency scale bounds, and adjust
                // top and bottom accordingly.
                const double maxRatio = findMaxRatio(mFreqSelPin, rate);
                double ratio = frequency / mFreqSelPin;
                if (ratio < 1.0) {
                    ratio = 1.0 / ratio;
                }
                ratio = std::min(maxRatio, ratio);
                viewInfo.selectedRegion.setFrequencies(
                    mFreqSelPin / ratio, mFreqSelPin * ratio);
            }
        }
    } else {
        // Dragging of upper or lower.
        const bool bottomDefined
            =!(mFreqSelMode == FREQ_SEL_TOP_FREE && mFreqSelPin < 0);
        const bool topDefined
            =!(mFreqSelMode == FREQ_SEL_BOTTOM_FREE && mFreqSelPin < 0);
        if (!bottomDefined || (topDefined && mFreqSelPin < frequency)) {
            // Adjust top
            if (frequency == rate) {
                // snapped high; upper frequency is undefined
                viewInfo.selectedRegion.setF1(SelectedRegion::UndefinedFrequency);
            } else {
                viewInfo.selectedRegion.setF1(std::max(1.0, frequency));
            }

            viewInfo.selectedRegion.setF0(mFreqSelPin);
        } else {
            // Adjust bottom
            if (frequency < 1.0) {
                // snapped low; lower frequency is undefined
                viewInfo.selectedRegion.setF0(SelectedRegion::UndefinedFrequency);
            } else {
                viewInfo.selectedRegion.setF0(std::min(rate / 2.0, frequency));
            }

            viewInfo.selectedRegion.setF1(mFreqSelPin);
        }
    }
}

void SelectHandle::HandleCenterFrequencyClick(const ViewInfo& viewInfo,
                                              bool shiftDown, const std::shared_ptr<const WaveChannel>& pWc, double value)
{
    if (shiftDown) {
        // Disable time selection
        mSelStartValid = false;
        mFreqSelTrack = pWc;
        mFreqSelPin = value;
        mFreqSelMode = FREQ_SEL_DRAG_CENTER;
    } else {
#ifndef SPECTRAL_EDITING_ESC_KEY
        // Start center snapping
        // Turn center snapping on (the only way to do this)
        mFreqSelMode = FREQ_SEL_SNAPPING_CENTER;
        // Disable time selection
        mSelStartValid = false;
        mFrequencySnapper = std::make_shared<SpectrumAnalyst>();
        StartSnappingFreqSelection(*mFrequencySnapper, viewInfo, *pWc);
#endif
    }
}

void SelectHandle::StartSnappingFreqSelection(SpectrumAnalyst& analyst,
                                              const ViewInfo& viewInfo, const WaveChannel& wc)
{
    static const size_t minLength = 8;

    const double rate = wc.GetRate();

    // Grab samples, just for this track, at these times
    std::vector<float> frequencySnappingData;
    const auto start
        =wc.TimeToLongSamples(viewInfo.selectedRegion.t0());
    const auto end
        =wc.TimeToLongSamples(viewInfo.selectedRegion.t1());
    const auto length
        =std::min(frequencySnappingData.max_size(),
                  limitSampleBufferSize(10485760, // as in FreqWindow.cpp
                                        end - start));
    const auto effectiveLength = std::max(minLength, length);
    frequencySnappingData.resize(effectiveLength, 0.0f);
    wc.GetFloats(
        &frequencySnappingData[0],
        start, length, FillFormat::fillZero,
        // Don't try to cope with exceptions, just read zeroes instead.
        false);

    // Use same settings as are now used for spectrogram display,
    // except, shrink the window as needed so we get some answers

    const auto& settings = SpectrogramSettings::Get(wc);
    auto windowSize = settings.GetFFTLength();

    while (windowSize > effectiveLength) {
        windowSize >>= 1;
    }
    const int windowType = settings.windowType;

    analyst.Calculate(
        SpectrumAnalyst::Spectrum, windowType, windowSize, rate,
        &frequencySnappingData[0], length);

    // We can now throw away the sample data but we keep the spectrum.
}

void SelectHandle::MoveSnappingFreqSelection(
    AudacityProject* pProject, ViewInfo& viewInfo, int mouseYCoordinate,
    int trackTopEdge,
    int trackHeight, ChannelView& channelView, Track* const pTrack)
{
    if (pTrack
        && pTrack->GetSelected()
        && isSpectralSelectionView(channelView)) {
        auto pWc = channelView.FindChannel<const WaveChannel>();
        // Spectral selection track is always wave
        assert(pWc);
        auto& wc = *pWc;
        // PRL:
        // What would happen if center snapping selection began in one spectrogram track,
        // then continues inside another?  We do not then recalculate
        // the spectrum (as was done in StartSnappingFreqSelection)
        // but snap according to the peaks in the old track.

        // But if we always supply the original clicked track here that doesn't matter.
        const double rate = wc.GetRate();
        const double frequency
            =PositionToFrequency(wc, false, mouseYCoordinate,
                                 trackTopEdge, trackHeight);
        const double snappedFrequency
            =mFrequencySnapper->FindPeak(frequency, NULL);
        const double maxRatio = findMaxRatio(snappedFrequency, rate);
        double ratio = 2.0; // An arbitrary octave on each side, at most
        {
            const double f0 = viewInfo.selectedRegion.f0();
            const double f1 = viewInfo.selectedRegion.f1();
            if (f1 >= f0 && f0 >= 0) {
                // Preserve already chosen ratio instead
                ratio = sqrt(f1 / f0);
            }
        }
        ratio = std::min(ratio, maxRatio);

        mFreqSelPin = snappedFrequency;
        viewInfo.selectedRegion.setFrequencies(
            snappedFrequency / ratio, snappedFrequency * ratio);

        // A change here would affect what AdjustFreqSelection() does
        // in the prototype version where you switch from moving center to
        // dragging width with a click.  No effect now.
        mFreqSelTrack = pWc;

        // SelectNone();
        // SelectTrack(pTrack, true);
        TrackFocus::Get(*pProject).Set(pTrack);
    }
}

void SelectHandle::SnapCenterOnce(SpectrumAnalyst& analyst,
                                  ViewInfo& viewInfo, const WaveChannel& wc, bool up)
{
    const auto& settings = SpectrogramSettings::Get(wc);
    const auto windowSize = settings.GetFFTLength();
    const double rate = wc.GetRate();
    const double nyq = rate / 2.0;
    const double binFrequency = rate / windowSize;

    double f1 = viewInfo.selectedRegion.f1();
    double centerFrequency = viewInfo.selectedRegion.fc();
    if (centerFrequency <= 0) {
        centerFrequency = up ? binFrequency : nyq;
        f1 = centerFrequency * sqrt(2.0);
    }

    double ratio = f1 / centerFrequency;
    const int originalBin = floor(0.5 + centerFrequency / binFrequency);
    const int limitingBin = up ? floor(0.5 + nyq / binFrequency) : 1;

    // This is crude and wasteful, doing the FFT each time the command is called.
    // It would be better to cache the data, but then invalidation of the cache would
    // need doing in all places that change the time selection.
    StartSnappingFreqSelection(analyst, viewInfo, wc);
    double snappedFrequency = centerFrequency;
    int bin = originalBin;
    if (up) {
        while (snappedFrequency <= centerFrequency
               && bin < limitingBin) {
            snappedFrequency = analyst.FindPeak(++bin * binFrequency, NULL);
        }
    } else {
        while (snappedFrequency >= centerFrequency
               && bin > limitingBin) {
            snappedFrequency = analyst.FindPeak(--bin * binFrequency, NULL);
        }
    }

    // PRL:  added these two lines with the big TrackPanel refactor
    const double maxRatio = findMaxRatio(snappedFrequency, rate);
    ratio = std::min(ratio, maxRatio);

    viewInfo.selectedRegion.setFrequencies
        (snappedFrequency / ratio, snappedFrequency * ratio);
}

#if 0
// unused
void SelectHandle::ResetFreqSelectionPin
    (const ViewInfo& viewInfo, double hintFrequency, bool logF)
{
    switch (mFreqSelMode) {
    case FREQ_SEL_INVALID:
    case FREQ_SEL_SNAPPING_CENTER:
        mFreqSelPin = -1.0;
        break;

    case FREQ_SEL_PINNED_CENTER:
        mFreqSelPin = viewInfo.selectedRegion.fc();
        break;

    case FREQ_SEL_DRAG_CENTER:
    {
        // Re-pin the width
        const double f0 = viewInfo.selectedRegion.f0();
        const double f1 = viewInfo.selectedRegion.f1();
        if (f0 >= 0 && f1 >= 0) {
            mFreqSelPin = sqrt(f1 / f0);
        } else {
            mFreqSelPin = -1.0;
        }
    }
    break;

    case FREQ_SEL_FREE:
        // Pin which?  Farther from the hint which is the presumed
        // mouse position.
    {
        // If this function finds use again, the following should be
        // generalized using NumberScale

        const double f0 = viewInfo.selectedRegion.f0();
        const double f1 = viewInfo.selectedRegion.f1();
        if (logF) {
            if (f1 < 0) {
                mFreqSelPin = f0;
            } else {
                const double logf1 = log(std::max(1.0, f1));
                const double logf0 = log(std::max(1.0, f0));
                const double logHint = log(std::max(1.0, hintFrequency));
                if (std::abs(logHint - logf1) < std::abs(logHint - logf0)) {
                    mFreqSelPin = f0;
                } else {
                    mFreqSelPin = f1;
                }
            }
        } else {
            if (f1 < 0
                || std::abs(hintFrequency - f1) < std::abs(hintFrequency - f0)) {
                mFreqSelPin = f0;
            } else {
                mFreqSelPin = f1;
            }
        }
    }
    break;

    case FREQ_SEL_TOP_FREE:
        mFreqSelPin = viewInfo.selectedRegion.f0();
        break;

    case FREQ_SEL_BOTTOM_FREE:
        mFreqSelPin = viewInfo.selectedRegion.f1();
        break;

    default:
        wxASSERT(false);
    }
}

#endif
